安装组件:pnpm dlx shadcn@latest add button field input select checkbox。
在所有form.Field的外层,包裹一个<FieldGroup></FieldGroup>。然后form.Field上除了children的部分,都不需要改变。
form.Field的children部分,可以写成children属性的形式,也可以直接写在form.Field标签之间。
xxxxxxxxxx461<FieldGroup>2 <form.Field3 name="firstName"4 validators={{5 onChangeAsyncDebounceMs: 500,6 onChangeAsync: z.string().refine(7 async (value) => {8 await new Promise((r) => setTimeout(r, 1000));9 return value !== "John";10 },11 {12 error: "First name cannot be John (async check).",13 }14 ),15 }}16 listeners={{17 onChange: ({ value }) => {18 if (!value) {19 form.setFieldValue("lastName", "");20 }21 },22 }}>23 {(field) => {24 return (25 <Field>26 <FieldLabel htmlFor="firstName">First Name</FieldLabel>27 <Input28 id="firstName"29 type="text"30 placeholder="John Doe"31 value={field.state.value}32 onBlur={field.handleBlur}33 onChange={(e) => field.handleChange(e.target.value)}34 />35 <FieldDescription>Your First Name.</FieldDescription>36 {!field.state.meta.isValid && (37 <FieldError>38 {field.state.meta.errors39 .map((error) => error?.message)40 .join(", ")}41 </FieldError>42 )}43 </Field>44 );45 }}46 </form.Field>可以看到,使用了<Field></Field>、FieldLabel、FieldDescription、FieldError,这部分是通用的。
然后就是具体的交互组件,需要给定id值,id值也就是<form.Field name=""这里的值,反正要一样。然后就是数据的绑定,value、onChange、onBlur。
需要注意的是,不同的交互组件,上面绑定的属性名value、onChange、onBlur不一定是这三种,需要根据实际情况进行处理。可以在这里查询到:https://ui.shadcn.com/docs/forms/tanstack-form。
xxxxxxxxxx51checked={field.state.value}2onCheckedChange={(checked) =>3 field.handleChange(checked === true)4}5onBlur={field.handleBlur}文档案例:

老师案例效果:

从上面的学习,我们可以知道,代码量还是蛮大的,而且有很多可以重复使用的部分。这节课来学习重构form表单。
参考:https://tanstack.com/form/v1/docs/framework/react/guides/form-composition
如何将大型、复杂的表单拆解成可复用、可组合的小型组件,同时保持优秀的类型安全、性能和可维护性。这份指南(最新版本)强调使用自定义 Hook、预绑定组件、HOC 模式等高级模式,避免在实际项目中写一堆重复的 <form.Field> 样板代码。
| 模式名称 | 核心 API | 适用场景 | 复杂度 | 类型安全 | 推荐指数 |
|---|---|---|---|---|---|
| 自定义 Form Hook | createFormHook + createFormHookContexts | 整个应用统一的表单风格、预绑定组件 | ★★☆ | ★★★★★ | ★★★★★ |
| 预绑定 Field 组件 | fieldComponents + useFieldContext | 复用 、 | ★☆☆ | ★★★★★ | ★★★★★ |
| 预绑定 Form 组件 | formComponents + useFormContext | 统一的提交按钮、loading 状态组件 | ★☆☆ | ★★★★ | ★★★★ |
| 用 withForm 拆分子表单 | withForm HOC | 把大表单切成小块(个人信息、地址、支付等) | ★★★ | ★★★★★ | ★★★★★ |
| 用 withFieldGroup 复用字段组 | withFieldGroup HOC | 密码+确认密码、地址三连(省/市/区)等强相关字段 | ★★★ | ★★★★ | ★★★★ |
| 动态加载 + Tree-shaking | React.lazy + Suspense | 大型应用中按需加载表单组件 | ★★☆ | ★★★★ | ★★★ |
createFormHookContexts 是一个工厂函数,从 @tanstack/react-form 直接导入,它的作用是一次性创建并导出所有必要的 React Context 和 Context 消费 Hook,专门用于后续的自定义表单钩子(createFormHook)和预绑定组件的构建。
简单说:它帮你生成form 和 field 的上下文基础设施,让你后续可以写出类型安全、可复用、性能好的自定义表单组件,避免在每个地方都手动用 useForm + form.Field 写一堆重复代码。
xxxxxxxxxx91// src/hooks/form-context.ts2import { createFormHookContexts } from '@tanstack/react-form'34export const {5 fieldContext,6 formContext,7 useFieldContext,8 useFormContext,9} = createFormHookContexts()导出变量的作用:
fieldContext:用于包裹单个 Field 的 Provider,存储 FieldApi 实例formContext:用于包裹整个 Form 的 Provider,存储 FormApi 实例useFieldContext<T>():类型安全的 Hook,在自定义 Field 组件内部使用,拿到当前 Field 的 API(value、handleChange、meta 等)useFormContext():类似,但拿到整个 Form 的 API(state、handleSubmit 等)为什么需要它?(核心价值)
createFormHook 是一个工厂函数,它接收一个配置对象,然后返回一个自定义的 useXXXForm Hook(比如 useAppForm、useMyForm 等)。这个自定义 Hook 相比原生的 useForm 有以下超级优势:
进阶配置(常用选项详解)
| 配置项 | 类型/作用 | 是否必须 | 典型用法示例 |
|---|---|---|---|
| fieldContext | 来自 createFormHookContexts 的 Field Context | 必须 | 几乎所有自定义 Hook 都需要 |
| formContext | 来自 createFormHookContexts 的 Form Context | 必须 | 同上 |
| fieldComponents | Record<string, React.ComponentType> | 推荐 | 绑定你的 Input、Select、DatePicker 等 |
| formComponents | Record<string, React.ComponentType> | 可选 | 绑定 SubmitButton、ResetButton 等 |
| defaultOpts | Partial | 可选 | 设置全局 validatorAdapter、默认 validators 等 |
| validatorAdapter | ValidatorAdapter | 可选(可放 defaultOpts) | 全局 Zod/Yup/Valibot 等适配器 |
xxxxxxxxxx391// src/hooks/useAppForm.ts2import { createFormHook } from '@tanstack/react-form'3import { zodValidator } from '@tanstack/zod-form-adapter'4import { fieldContext, formContext } from './form-context' // 来自 createFormHookContexts56// 你自己封装的 UI 组件(推荐)7import { TextField } from '@/components/ui/TextField'8import { NumberField } from '@/components/ui/NumberField'9import { DatePickerField } from '@/components/ui/DatePickerField'10import { SubmitButton } from '@/components/ui/SubmitButton'1112export const { useAppForm } = createFormHook({13 // 必须传入前面 createFormHookContexts 创建的 context14 fieldContext,15 formContext,1617 // 核心:预绑定你常用的字段组件18 // 之后就可以用 form.AppField({ children: f => <f.TextField ... /> })19 fieldComponents: {20 TextField,21 NumberField,22 DatePickerField,23 },2425 // 可选:预绑定表单级组件(如统一的提交按钮)26 formComponents: {27 SubmitButton,28 },2930 // 可选:全局默认配置(所有 useAppForm 实例都会继承)31 defaultOpts: {32 validatorAdapter: zodValidator(),33 validators: {34 onChange: true, // 全局默认开启 onChange 校验35 onBlur: true,36 },37 // 可以加默认的 transform、默认值处理等38 },39})然后全局使用 useAppForm 而不是 useForm,自动带上预设组件。
xxxxxxxxxx391// registerForm.tsx2import { useAppForm } from '@/hooks/useAppForm'34function UserRegisterForm() {5 const form = useAppForm({6 defaultValues: {7 name: '',8 age: 0,9 birthDate: new Date(),10 },11 // 这里可以覆盖或追加全局配置12 })1314 return (15 <form16 onSubmit={(e) => {17 e.preventDefault()18 e.stopPropagation()19 form.handleSubmit()20 }}21 >22 {/* 使用预绑定的组件,超级简洁 */}23 <form.AppField name="name">24 {field => <field.TextField label="姓名" />}25 </form.AppField>2627 <form.AppField name="age">28 {field => <field.NumberField label="年龄" min={0} />}29 </form.AppField>3031 <form.AppField name="birthDate">32 {field => <field.DatePickerField label="出生日期" />}33 </form.AppField>3435 {/* 统一的提交按钮 */}36 <form.SubmitButton>保存</form.SubmitButton>37 </form>38 )39}xxxxxxxxxx111src/2 hooks/3 form-context.ts ← createFormHookContexts()4 useAppForm.ts ← createFormHook() 返回 useAppForm5 components/6 ui/7 form-fields/8 TextField.tsx9 NumberField.tsx10 DatePickerField.tsx11 SubmitButton.tsx在createFormHook的fieldComponents属性里面,要传入定义好的Field Components,才能直接使用。也就是<form.Field></form.Field>的children部分。
重点:
接收固定值的参数,比如说label、description、id。
在创建field Components的时候,使用const field = useFieldContext<string>();获取到field。它提供了:
需要注册到 createFormHook 中
在使用时,需要将<form.Field改为<form.AppField,这里的AppField里面的App是你自己起的命名空间(namespace),可以改成任何名字,比如 My、Custom、Pro 等。
1、创建field component
xxxxxxxxxx441// src/components/ui/form/TextInput.tsx23import {4 Field,5 FieldDescription,6 FieldError,7 FieldLabel,8} from "@/components/ui/field";9import { useFieldContext } from "@/hooks/form-context";10import { Input } from "../input";1112export default function TextInput({13 label,14 description,15 id,16 type = "text",17}: {18 label: string;19 description?: string;20 id: string;21 type?: string;22}) {23 const field = useFieldContext<string>();2425 return (26 <Field>27 <FieldLabel htmlFor={id}>{label}</FieldLabel>28 <Input29 id={id}30 type={type}31 placeholder={`Please enter ${label}`}32 value={field.state.value}33 onBlur={field.handleBlur}34 onChange={(e) => field.handleChange(e.target.value)}35 />36 {description && <FieldDescription>{description}</FieldDescription>}37 {!field.state.meta.isValid && (38 <FieldError>39 {field.state.meta.errors.map((error) => error?.message).join(", ")}40 </FieldError>41 )}42 </Field>43 );44}2、在createFormHook里面注册field component
xxxxxxxxxx141// src/hooks/form.ts23import { createFormHook } from "@tanstack/react-form";4import { fieldContext, formContext } from "./form-context";5import TextInput from "@/components/ui/form/TextInput";67export const { useAppForm, withForm, withFieldGroup } = createFormHook({8 fieldContext: fieldContext,9 formContext: formContext,10 fieldComponents: {11 TextInput: TextInput,12 },13 formComponents: {},14});3、在form中使用这个组件
xxxxxxxxxx391// src/lib/RegisterForm.tsx23import { useAppForm } from "@/hooks/form";45// 必须使用自定义的 useAppForm6const form = useAppForm({7 registerFormOpts,8 9});1011<form.AppField12 name="firstName"13 validators={{14 onChangeAsyncDebounceMs: 500,15 onChangeAsync: z.string().refine(16 async (value) => {17 await new Promise((r) => setTimeout(r, 1000));18 return value !== "John";19 },20 {21 error: "First name cannot be John (async check).",22 }23 ),24 }}25 listeners={{26 onChange: ({ value }) => {27 if (!value) {28 form.setFieldValue("lastName", "");29 }30 },31 }}>32 {(field) => (33 <field.TextInput34 id="firstName"35 label="First Name"36 description="Your First Name."37 />38 )}39</form.AppField>可以对比一下,第一张是之前的代码,第二张是之后的代码,代码简洁多了,可以让我们专心写逻辑代码。


Form Components 必须使用 useFormContext来获取整个 FormApi。
它给你完整的 FormApi,包括:
Form Components 通常不管理自己的状态,而是读取 form.state 并据此渲染。利用 form.state 的响应式特性(基于 TanStack Store),组件会自动在相关状态变化时重渲染。常见响应项:
必须注册到 createFormHook 的 formComponents里面才能使用
为了能够使用form components,需要在表单外层加上form.AppForm,也就是这样:
xxxxxxxxxx51<form.AppForm>2 <form>3 ......4 </form>5</form.AppForm>使用时,直接<form.xxx来使用,xxx是注册的组件名,不用像使用fieldComponents那样做。
| 组件名称 | 重点关注的内容 | 典型代码片段 |
|---|---|---|
| SubmitButton | isSubmitting / isValid / loading 状态 | disabled + 文字切换 |
| ResetButton | form.reset() + 确认弹窗(可选) | onClick={() => form.reset()} |
| FormErrorSummary | 收集所有 touchedErrors 并显示(全局错误汇总) | form.state.meta.errors |
| FormLoadingOverlay | 当 isSubmitting 时显示遮罩 | {form.state.isSubmitting && |
| FormStatus | 显示“已保存”“保存中”“有错误”等提示 | 结合 isDirty / isSubmitting / errors |
1、创建form component
xxxxxxxxxx281// src/components/ui/form/SubmitButton.tsx23import { useFormContext } from "@/hooks/form-context";4import { Button } from "../button";56export default function SubmitButton() {7 const form = useFormContext();8 return (9 <form.Subscribe selector={(state) => state.canSubmit}>10 {(canSubmit) => (11 <div>12 <Button13 disabled={!canSubmit}14 type="submit"15 onClick={() => form.handleSubmit({ submitAction: "save" })}>16 Save17 </Button>18 <Button19 disabled={!canSubmit}20 type="submit"21 onClick={() => form.handleSubmit({ submitAction: "goback" })}>22 Save & Go back23 </Button>24 </div>25 )}26 </form.Subscribe>27 );28}2、注册
xxxxxxxxxx171// src/hooks/form.ts23import { createFormHook } from "@tanstack/react-form";4import { fieldContext, formContext } from "./form-context";5import TextInput from "@/components/ui/form/TextInput";6import SubmitButton from "@/components/ui/form/SubmitButton";78export const { useAppForm, withForm, withFieldGroup } = createFormHook({9 fieldContext: fieldContext,10 formContext: formContext,11 fieldComponents: {12 TextInput: TextInput,13 },14 formComponents: {15 SubmitButton: SubmitButton,16 },17});3、使用
xxxxxxxxxx161// src/lib/register-form/RegisterForm.tsx23<form.AppForm>4 <form5 onSubmit={(e) => {6 e.preventDefault();7 form.handleSubmit({ key: "value" });8 }}>9 <FieldGroup>10 ......11 </FieldGroup>12 13 {/* 可以看到,使用起来非常简单 */}14 <form.SubmitButton />15 </form>16</form.AppForm>可以看到,界面正常,交互正常。

在定义createFormHook的时候,我们引入了很多field components和form components,一次性导入这么多组件可能造成性能问题,所以可以使用React.lazy 和 动态导入(Dynamic Import),来“按需加载”,而不是一次性下载所有内容。
1、在定义createFormHook的文件中,使用按需加载
xxxxxxxxxx241// src/hooks/form.ts23import { createFormHook } from "@tanstack/react-form";4import { fieldContext, formContext } from "./form-context";5import { lazy } from "react";67const TextInput = lazy(() => import("@/components/ui/form/TextInput"));8const SubmitButton = lazy(() => import("@/components/ui/form/SubmitButton"));9const CustomCheckbox = lazy(() => import("@/components/ui/form/Checkbox"));10const DropDown = lazy(() => import("@/components/ui/form/Select"));1112export const { useAppForm, withForm, withFieldGroup } = createFormHook({13 fieldContext: fieldContext,14 formContext: formContext,15 fieldComponents: {16 TextInput: TextInput,17 Checkbox: CustomCheckbox,18 Select: DropDown,19 },20 formComponents: {21 SubmitButton: SubmitButton,22 },23});242、必须配合 <Suspense> 使用
在使用这些组件的地方,你必须包裹 Suspense 并在 fallback 中提供加载状态(如 Loading 菊花图),否则 React 会报错。
xxxxxxxxxx161// App.tsx23import { Suspense } from "react";4import RegisterForm from "./lib/register-form/RegisterForm";56function App() {7 return (8 <>9 <Suspense fallback={<div>Loading...</div>}>10 <RegisterForm />11 </Suspense>12 </>13 );14}1516export default App;可以看到,在出现registerForm之前,有一个短暂的loading效果。

注意:
- 粒度过细的问题: 图中的
TextInput、Select、Checkbox通常是非常小的基础组件。如果这些组件代码量极小(比如只有几 KB),过度拆分反而可能变慢。因为发起多次 HTTP 请求的开销可能大于一次性下载一个小文件的开销。建议:通常建议对“大型页面路由”或“超重型第三方库(如 Chart.js, Rich Text Editor)”使用
lazy。对于这种轻量级的 UI 基础组件,通常直接import性能反而更好。
- 闪烁问题: 由于是异步加载,组件出现时可能会有短暂的“白屏”或“跳动”。在表单这种对交互连贯性要求高的场景下,需要精心设计 Loading 占位符(Skeleton)。
在 TanStack Form 中,withForm 是一个非常强大的 HOC(Higher-Order Component,高阶组件),它的核心作用是:把一个“普通的 React 组件”包装成“带有独立表单状态的子表单组件”,让大型复杂表单可以轻松拆分成多个可复用、可独立维护的小型表单模块,同时保持完整的类型安全和表单上下文。
简单来说:
withForm 就是 TanStack Form 提供的“子表单工厂”,专门用来解决“一个页面有多个表单区域,但又不想全部塞到一个巨型 useForm 里”的痛点。
| 作用 | 具体说明 | 实际价值(为什么值得用) |
|---|---|---|
| 1. 独立表单状态与默认值 | 每个 withForm 包装的组件都有自己的 defaultValues 和独立的 FormApi | 子表单可以独立重置、独立校验、独立提交 |
| 2. 字段名映射(Field Mapping) | 支持把父表单的字段路径映射到子表单的局部字段名 | 比如父表单有 user.address.street,子表单里写 street 就行 |
| 3. 上下文自动继承 | 自动继承父表单的 validatorAdapter、onSubmit 等配置 | 子表单不用重复写全局配置,保持一致性 |
| 4. 类型安全极强 | 子表单的 defaultValues 只用于类型推导,运行时不生效 | 字段名拼错编译期就报错,开发体验极佳 |
| 5. 支持嵌套 & 复用 | 可以嵌套使用、跨页面复用,甚至动态加载(lazy) | 适合大型表单、向导式多步表单、复用表单块 |
| 6. 性能友好 | 子表单的上下文是静态的,不会引起多余重渲染 | 大表单拆分后性能反而更好 |
xxxxxxxxxx441// AddressForm.tsx2import { withForm } from '@tanstack/react-form'34const AddressForm = withForm({5 // 只用于类型推导,运行时不生效6 defaultValues: {7 street: '',8 city: '',9 zip: '',10 },11 props: {12 title: String, // 可以接收外部 props13 },14 render: function Render({ form, title }) {15 return (16 <div className="border p-6 rounded-lg">17 <h3 className="text-lg font-bold mb-4">{title}</h3>18 19 <form.AppField name="street">20 {f => <f.TextField label="街道" />}21 </form.AppField>22 23 <form.AppField name="city">24 {f => <f.TextField label="城市" />}25 </form.AppField>26 27 <form.AppField name="zip">28 {f => <f.TextField label="邮编" />}29 </form.AppField>30 </div>31 )32 },33})3435// 使用36<AddressForm 37 form={parentForm} 38 fields={{ 39 street: 'shipping.street', 40 city: 'shipping.city', 41 zip: 'shipping.zip' 42 }}43 title="收货地址"44/>withForm 最常用的几个参数及其详细讲解:
| 参数名 | 类型 | 是否必须 | 主要作用 | 实际使用场景 & 注意事项 |
|---|---|---|---|---|
| defaultValues | object / () => object | 推荐 | 只用于类型推导,定义子表单的字段结构和类型,帮助 TypeScript 推导字段名和值类型 | 必须写完整结构(不管有多少层子表单嵌套,都是最终的那个表单的类型),但运行时不生效(实际值来自父表单)。常用于字段名自动补全和类型安全。 |
| props | object (类型定义) | 可选 | 定义组件接收的额外 props 类型(如 title、className 等),增强组件可配置性 | 让子表单组件更灵活,例如 |
| render | function({ form, props }) => JSX | 必须 | 实际渲染函数,接收子表单的 form(FormApi 实例)和外部 props,返回 JSX | 这里写真正的 UI 和 <form.AppField> 使用逻辑。必须是命名函数(function Render() {}),避免 ESLint hooks 规则报错。 |
| fields | object (运行时传入) | 必须(当有字段映射时) | 字段路径映射,把父表单的深层字段映射到子表单的局部字段名 | 核心功能!例如 { street: 'shipping.street' },子表单写 street 就映射到父级的 shipping.street |
| validatorAdapter | ValidatorAdapter | 可选 | 子表单专属的校验适配器(通常继承父表单) | 极少单独指定,大多数情况继承全局的(如 zodValidator) |
| onSubmit | (values) => Promise | 可选 | 子表单独立的提交处理器 | 很少用,通常由父表单统一处理。适合特殊子表单需要独立提交的场景 |
| transform | (values) => any | 可选 | 在子表单提交前对值进行转换(类似 schema transform) | 例如把局部值合并回父表单结构,或格式化数据 |
重点就是defaultValues和render参数。
xxxxxxxxxx161const PaymentForm = withForm({2 defaultValues: { cardNumber: '', expiry: '', cvv: '' },3 render: ({ form }) => (4 <>5 <h3>支付信息</h3>6 <form.AppField name="cardNumber">{f => <f.TextField label="卡号" />} </form.AppField>7 8 {/* 嵌套另一个子表单 */}9 <AddressForm 10 form={form} 11 fields={{ street: 'billing.street', city: 'billing.city' }}12 title="账单地址"13 />14 </>15 ),16})xxxxxxxxxx231function FamilyMembers() {2 const form = useAppForm()3 const members = form.useFieldArray('familyMembers')45 return (6 <div>7 {members.fields.map((field, index) => (8 <MemberForm9 key={field.key}10 form={form}11 fields={{12 name: `familyMembers.${index}.name`,13 age: `familyMembers.${index}.age`,14 }}15 onRemove={() => members.remove(index)}16 />17 ))}18 <button onClick={() => members.append({ name: '', age: 0 })}>19 添加成员20 </button>21 </div>22 )23}defaultValues 只用于类型推导,不会覆盖父表单的值案例中,skills部分的代码比较多,将这部分拆分为子表单。
1、创建一个子表单组件
其实很简单,就是使用withForm,将之前定义的registerFormOpts拿过来,然后在render里面将form.Field相关的拿过来即可。
xxxxxxxxxx691// src/lib/register-form/RegisterSkills.tsx23import { withForm } from "@/hooks/form";4import { registerFormOpts } from "./shared";5import { Button } from "@/components/ui/button";6import {7 FieldDescription,8 FieldGroup,9 FieldLegend,10 FieldSet,11} from "@/components/ui/field";1213export const RegisterSkills = withForm({14 registerFormOpts,15 render: function Render({ form }) {16 return (17 <form.AppField name="skills" mode="array">18 {(field) => {19 return (20 <FieldSet>21 <FieldLegend>Skills</FieldLegend>22 <FieldDescription>Your skills and expertise.</FieldDescription>23 <FieldGroup>24 {field.state.value.map((skill, index) => {25 return (26 <div key={skill.id} className="p-4 bg-gray-100 rounded">27 <p className="mb-4 font-bold">{skill.name}</p>28 <form.AppField29 key={index}30 name={`skills[${index}].level`}>31 {(subField) => {32 return (33 <subField.Select34 label="Skill Level"35 id={`skills-${index}-level`}36 />37 );38 }}39 </form.AppField>40 <Button41 className="mt-2"42 variant="destructive"43 type="button"44 onClick={() => field.removeValue(index)}>45 Remove46 </Button>47 </div>48 );49 })}50 <Button51 onClick={() =>52 field.pushValue({53 name: `New Skill ${field.state.value.length + 1}`,54 id: crypto.randomUUID(),55 level: "beginner",56 })57 }58 variant="secondary"59 type="button">60 Add Skill61 </Button>62 </FieldGroup>63 </FieldSet>64 );65 }}66 </form.AppField>67 );68 },69});2、使用子表单
引入之后,直接使用,指定form属性即可。

是不是感觉很简单。
在withForm的例子中,我们可以将一些filed放到一个子表单里面去。但是这个子表单的defaultValues属性(使用的是父表单的defaultValues)决定了,这个子表单只能用于一个具体的父表单,因为表单不可能defaultValues都相同(这么多状态变量),这可以理解吧。
withFieldGroup → 更轻量,只处理一小组字段,不创建独立 FormApi,完全共享父表单的上下文和状态。可以解决“密码确认”“日期范围”“地址三连”等强耦合字段组在多个表单/页面重复出现时的代码重复问题。
1、定义 fieldGroup 的schema
xxxxxxxxxx151// src/lib/shared/schema.ts23import z from "zod";45// 为 fieldGroup 专门定义schema,这样在父表单中使用它的时候,可以直接在父表单的schema上面,使用and拼接即可。6// 不直接将变量校验规则定义在父表单的schema上面,是因为 fieldGroup 上大概率有 refine 校验,如果定义在父表单的 schema 上面,是不是要直接定义refine,那样岂不是使用一次定义一次?太麻烦了,所以这种方式正好。7export const passwordsSchema = z8 .object({9 password: z.string().min(6, "Password should be at least 6 characters"),10 confirmPassword: z.string(),11 })12 .refine((data) => data.password === data.confirmPassword, {13 error: "Passwords do not match",14 path: ["confirmPassword"], // 指定错误发生时,在哪个field展示错误信息15 });2、使用 withFieldGroup 定义组件
重点就是defaultValues和render函数。
xxxxxxxxxx461// src/components/ui/form/PasswordFields.tsx23import { withFieldGroup } from "@/hooks/form";4import { passwordsSchema } from "@/lib/shared/schema";5import z from "zod";67const defaultValues: z.infer<typeof passwordsSchema> = {8 password: "",9 confirmPassword: "",10};1112export const PasswordFields = withFieldGroup({13 defaultValues,14 // 渲染函数,接收 group(字段组上下文)和 props15 render: function Render({ group }) {16 return (17 <>18 <group.AppField name="password">19 {(field) => {20 return (21 <field.TextInput22 label="Password"23 id="password"24 type="password"25 description="Your password (must be at least 8 characters long)"26 />27 );28 }}29 </group.AppField>3031 <group.AppField name="confirmPassword">32 {(field) => {33 return (34 <field.TextInput35 label="Confirm Password"36 id="confirmPassword"37 type="password"38 description="Confirm your password."39 />40 );41 }}42 </group.AppField>43 </>44 );45 },46});3、修改父表单的schema,使用and拼接 fieldGroup 的schema

注意:只是父表单里面的schema使用and拼接,父表单的默认值里面还是需要定义相关的变量的。因为在使用 fieldGroup的时候,要制定fields参数的。
4、使用组件
直接使用即可,注意设置form,就是父组件的form。以及重点:设置 fields 映射。
例子:
xxxxxxxxxx101// 在任意表单中使用2<PasswordGroup3 form={parentForm}4 // 字段映射:把组内局部名映射到父表单的实际路径。对象里面的key就是 fieldGroup 里面的变量名,value就是父表单里面的变量名。5 fields={{6 password: 'user.credentials.password',7 confirm_password: 'user.credentials.confirmPassword'8 }}9 title="设置密码"10/>
可以看到,表单正常使用。

最佳实践:
- 优先级建议:先用自定义 useAppForm → 再用预绑定组件 → 大表单用 withForm → 字段组用 withFieldGroup
- 类型安全:defaultValues 在 HOC 中只用于类型推导,不参与运行时
- 性能:context 值是静态的(TanStack Store 驱动),子组件不会因无关变化重渲染
- ESLint 友好:render 函数要用命名函数(function Render() {}),避免 hooks 规则报错
- 字段映射限制:目前只支持对象映射(不能直接映射数组或顶层 Record)
- 异步/懒加载:结合 React.lazy + Suspense 做代码分割,非常适合大型表单页面